﻿using System;
using System.Collections.Generic;
using System.Reflection;
using MugenInjection.Delegates;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Parameters;

// ReSharper disable CheckNamespace

namespace MugenInjection.Core.Components
// ReSharper restore CheckNamespace
{
    /// <summary>
    /// </summary>
    public class FactoryAndLazyConverter : ConverterBase
    {
        #region Nested type: FactoryDelegateCache

        private sealed class FactoryDelegateCache : IEquatable<FactoryDelegateCache>
        {
            #region Equality members

            /// <summary>
            /// Indicates whether the current object is equal to another object of the same type.
            /// </summary>
            /// <returns>
            /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
            /// </returns>
            /// <param name="other">An object to compare with this object.
            ///                 </param>
            public bool Equals(FactoryDelegateCache other)
            {
                if (ReferenceEquals(null, other)) return false;
                if (ReferenceEquals(this, other)) return true;
                return Equals(Member, other.Member) && Equals(Parameter, other.Parameter) &&
                       Equals(Service, other.Service) && Equals(Injector, other.Injector) &&
                       Equals(Binding, other.Binding);
            }

            /// <summary>
            /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
            /// </summary>
            /// <returns>
            /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
            /// </returns>
            /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>. 
            ///                 </param><exception cref="T:System.NullReferenceException">The <paramref name="obj"/> parameter is null.
            ///                 </exception>
            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                return obj is FactoryDelegateCache && Equals((FactoryDelegateCache)obj);
            }

            /// <summary>
            /// Serves as a hash function for a particular type. 
            /// </summary>
            /// <returns>
            /// A hash code for the current <see cref="T:System.Object"/>.
            /// </returns>
            public override int GetHashCode()
            {
                unchecked
                {
                    var hashCode = (Member != null ? Member.GetHashCode() : 0);
                    hashCode = (hashCode * 397) ^ (Parameter != null ? Parameter.GetHashCode() : 0);
                    hashCode = (hashCode * 397) ^ (Service != null ? Service.GetHashCode() : 0);
                    hashCode = (hashCode * 397) ^ (Injector != null ? Injector.GetHashCode() : 0);
                    hashCode = (hashCode * 397) ^ (Binding != null ? Binding.GetHashCode() : 0);
                    return hashCode;
                }
            }

            public static bool operator ==(FactoryDelegateCache left, FactoryDelegateCache right)
            {
                return Equals(left, right);
            }

            public static bool operator !=(FactoryDelegateCache left, FactoryDelegateCache right)
            {
                return !Equals(left, right);
            }

            #endregion

            #region Fields

            public readonly MemberInfo Member;
            public readonly ParameterInfo Parameter;
            public readonly Type Service;
            public readonly IBinding Binding;
            public readonly IInjector Injector;

            #endregion

            #region Constructor

            /// <summary>
            ///     Initializes a new instance of the <see cref="FactoryDelegateCache" /> class.
            /// </summary>
            public FactoryDelegateCache(Type type, IBindingContext bindingContext, IBinding binding)
            {
                Service = type;
                Binding = binding;
                Member = bindingContext.Member;
                Parameter = bindingContext.ParameterInfo;
                Injector = bindingContext.CallInjector;
            }

            #endregion
        }

        #endregion

        #region Nested type: FactoryResolve

        private sealed class FactoryResolve
        {
            #region Fields

            private readonly ConverterContext _converterContext;
            private IBindingContext _context;

            #endregion

            #region Constructor

            /// <summary>
            ///     Initializes a new instance of the <see cref="FactoryResolve" /> class.
            /// </summary>
            public FactoryResolve(ConverterContext converterContext)
            {
                _converterContext = converterContext;
                _context = converterContext.BindingContext;
                _context.CallInjector.Disposed += CallInjectorOnDisposed;
            }

            private void CallInjectorOnDisposed(IDisposableObject disposableObject)
            {
                disposableObject.Disposed -= CallInjectorOnDisposed;
                _context = null;
            }

            #endregion

            #region Methods

            public object Invoke(object[] objects)
            {
                IBindingContext bindingContext;
                if (objects.Length == 0)
                    bindingContext = MergeParameters(_context, null, null);
                else
                {
                    IDictionary<string, object> specialParamters = null;
                    var parameters = new List<IInjectionParameter>();
                    int actualPosition = 0;
                    for (int index = 0; index < objects.Length; index++)
                    {
                        var o = objects[index];
                        var injectionParameters = o as IEnumerable<IInjectionParameter>;
                        if (injectionParameters != null)
                        {
                            parameters.AddRange(injectionParameters);
                            continue;
                        }

                        var settings = o as ISettings;
                        if (settings != null)
                        {
                            specialParamters = settings.ToDictionary();
                            continue;
                        }

                        int position = actualPosition;
                        parameters.Add(new ConstructorParameter(position, o));
                        actualPosition++;
                    }
                    bindingContext = MergeParameters(_context, specialParamters, parameters);
                }
                if (bindingContext == null)
                    throw new ObjectDisposedException("IInjector", "The injector that created this method was disposed.");

                return _converterContext.Activator(bindingContext);
            }

            private static BindingContext MergeParameters(IBindingContext bindingContext, IDictionary<string, object> dictionary, List<IInjectionParameter> parameters)
            {
                if (bindingContext == null)
                    return null;
                if (parameters != null && bindingContext.Parameters != null)
                    parameters.AddRange(bindingContext.Parameters);

                IList<IInjectionParameter> contextParameters;
                if (parameters != null)
                    contextParameters = parameters;
                else if (bindingContext.Parameters != null)
                    contextParameters = bindingContext.Parameters;
                else
                    contextParameters = InjectorUtilsInternal.EmptyInjectionParams;

                var dict = InjectorUtilsInternal.GetDictionary(bindingContext);
                if (dictionary != null)
                {
                    foreach (var o in dictionary)
                        dict[o.Key] = o.Value;
                }
                return new BindingContext(bindingContext.Service, bindingContext.Member, bindingContext.ParameterInfo,
                                          bindingContext.Injector, bindingContext.CallInjector, contextParameters, dict)
                {
                    ParentBindingContext = bindingContext.ParentBindingContext
                };
            }

            #endregion
        }

        #endregion

        #region Fields

        private readonly Dictionary<FactoryDelegateCache, Delegate> _factoryDelegateCache;
        private readonly NotifyCollectionImpl<Type> _factoryDelegateTypes;
        private readonly NotifyCollectionImpl<Type> _lazyDelegateTypes;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="FactoryAndLazyConverter" /> class.
        /// </summary>
        public FactoryAndLazyConverter()
            : base(DefaultPriority, ServiceType.Complex | ServiceType.Activatable | ServiceType.Resolvable)
        {
            IsCacheContextWithBinding = true;
            _factoryDelegateCache = new Dictionary<FactoryDelegateCache, Delegate>();
            _factoryDelegateTypes = new NotifyCollectionImpl<Type>
                                        {
                                            typeof (Func<>),
                                            typeof (Func<,>),
                                            typeof (Func<,,>),
                                            typeof (Func<,,,>),
#if !NOLAZY
                                            typeof(Func<,,,,>),
                                            typeof(Func<,,,,,>),
                                            typeof(Func<,,,,,,>),
                                            typeof(Func<,,,,,,,>),
                                            typeof(Func<,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,,,,,,>),
                                            typeof(Func<,,,,,,,,,,,,,,,,>)                                            
#endif
                                        };
            _lazyDelegateTypes = new NotifyCollectionImpl<Type>();
#if !NOLAZY
            _lazyDelegateTypes.Add(typeof(Lazy<>));
#endif

            _factoryDelegateTypes.Added += FactoryTypesOnAdded;
            _factoryDelegateTypes.Changed += FactoryTypesOnChanged;
            _factoryDelegateTypes.Removed += FactoryTypesOnRemoved;

            _lazyDelegateTypes.Added += LazyTypesOnAdded;
            _lazyDelegateTypes.Changed += LazyTypesOnChanged;
            _lazyDelegateTypes.Removed += LazyTypesOnRemoved;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets value that responsible to cache context with binding.
        /// </summary>
        public bool IsCacheContextWithBinding { get; set; }
        
        /// <summary>
        ///     Gets types of factory delegates.
        /// </summary>
        public virtual IList<Type> FactoryDelegateTypes
        {
            get { return _factoryDelegateTypes; }
        }

        /// <summary>
        ///   Gets types of lazy delegates.
        /// </summary>
        public virtual IList<Type> LazyDelegateTypes
        {
            get
            {
                return _lazyDelegateTypes;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        ///     Occurs when factory type was added.
        /// </summary>
        protected virtual void FactoryTypesOnAdded(Type type)
        {
            Validate.ArgumentNotNull(type, "type");
            Validate.IsAssignable(typeof(Delegate), type);
#if !NETFX_CORE
            if (!type.IsGenericType)
#else
            if (!type.GetTypeInfo().IsGenericType)            
#endif
                throw new ArgumentException("The factory type should be a generic.");
            ClearCache();
            OnSupportedTypeChanged(TypeChangedAction.Add, type);
        }

        /// <summary>
        ///     Occurs when factory type was changed.
        /// </summary>
        protected virtual void FactoryTypesOnChanged(Type oldValue, Type newValue)
        {
            OnSupportedTypeChanged(TypeChangedAction.Remove, oldValue);
            FactoryTypesOnAdded(newValue);
        }

        /// <summary>
        ///     Occurs when factory type was removed.
        /// </summary>
        protected virtual void FactoryTypesOnRemoved(Type type)
        {
            ClearCache();
            OnSupportedTypeChanged(TypeChangedAction.Remove, type);
        }

        /// <summary>
        ///     Occurs when lazy type was added.
        /// </summary>
        protected virtual void LazyTypesOnAdded(Type type)
        {
            Validate.ArgumentNotNull(type, "type");
#if !NETFX_CORE
            if (!type.IsGenericType)
#else
            if (!type.GetTypeInfo().IsGenericType)            
#endif
                throw new ArgumentException("The lazy type should be a generic.");
            ClearCache();
            OnSupportedTypeChanged(TypeChangedAction.Add, type);
        }

        /// <summary>
        ///     Occurs when lazy type was changed.
        /// </summary>
        protected virtual void LazyTypesOnChanged(Type oldValue, Type newValue)
        {
            OnSupportedTypeChanged(TypeChangedAction.Remove, oldValue);
            LazyTypesOnAdded(newValue);
        }

        /// <summary>
        ///     Occurs when lazy type was removed.
        /// </summary>
        protected virtual void LazyTypesOnRemoved(Type type)
        {
            ClearCache();
            OnSupportedTypeChanged(TypeChangedAction.Remove, type);
        }

        /// <summary>
        ///     Determines whether the specified type is a factory.
        /// </summary>
        /// <param name="serviceType">The specified type.</param>
        /// <param name="originalServiceType">The original service type, if is a lfactory.</param>
        /// <returns>
        ///     If <c>true</c> is a factory; otherwise <c>false</c>.
        /// </returns>
        protected virtual bool IsFactory(Type serviceType, out Type originalServiceType)
        {
            bool isFactory = false;
            for (int index = 0; index < _factoryDelegateTypes.Count; index++)
            {
                Type type = _factoryDelegateTypes[index];
                if (!InjectorUtilsInternal.IsEqualGenericDefinition(serviceType, type)) continue;
                isFactory = true;
                break;
            }
            if (isFactory)
                originalServiceType = serviceType.GetMethod("Invoke").ReturnType;
            else
                originalServiceType = null;
            return originalServiceType != null;
        }

        /// <summary>
        ///     Determines whether the specified type is a lazy-proxy.
        /// </summary>
        /// <param name="serviceType">The specified type.</param>
        /// <param name="originalServiceType">The original service type, if is a lazy-proxy.</param>
        /// <param name="constructor">The constructor to activate lazy-proxy, if is a lazy-proxy.</param>
        /// <returns>
        ///     If <c>true</c> is a lazy-proxy; otherwise <c>false</c>.
        /// </returns>
        protected virtual bool IsLazyProxy(Type serviceType, out Type originalServiceType,
                                           out ConstructorInfo constructor)
        {
            originalServiceType = null;
            constructor = null;
#if !NETFX_CORE
            if (!serviceType.IsGenericType)
#else
            if (!serviceType.GetTypeInfo().IsGenericType)
#endif
                return false;

            bool isLazy = false;
            for (int index = 0; index < _lazyDelegateTypes.Count; index++)
            {
                Type type = _lazyDelegateTypes[index];
                if (!InjectorUtilsInternal.IsEqualGenericDefinition(serviceType, type)) continue;
                isLazy = true;
                break;
            }
            if (isLazy)
            {
                isLazy = false;
                Type[] genericArguments = serviceType.GetGenericArguments();
                originalServiceType = genericArguments[0];
#if NETFX_CORE
                foreach (ConstructorInfo constructorInfo in serviceType.GetTypeInfo().DeclaredConstructors)
#else
                foreach (ConstructorInfo constructorInfo in serviceType.GetConstructors())
#endif
                {
                    ParameterInfo[] parameterInfos = constructorInfo.GetParameters();
#if NETFX_CORE
                    if (parameterInfos.Length != 1 || !parameterInfos[0].ParameterType.GetTypeInfo().IsGenericType) continue;
#else
                    if (parameterInfos.Length != 1 || !parameterInfos[0].ParameterType.IsGenericType) continue;
#endif

                    ParameterInfo parameterInfo = parameterInfos[0];
                    Type type;
                    if (!IsFactory(parameterInfo.ParameterType, out type)) continue;
                    constructor = constructorInfo;
                    isLazy = true;
                    break;
                }
            }
            return isLazy;
        }

        /// <summary>
        ///     Converts the specified object to factory-proxy.
        /// </summary>
        /// <param name="inputValue">
        ///     The specified object to convert, by default is always <see cref="IBindingContext" />.
        /// </param>
        /// <param name="factoryType">The specified type of factory.</param>
        /// <param name="originalType">The specified type of object.</param>
        /// <returns>An instance of managed-wrapper.</returns>
        protected virtual object ConvertToFunc(object inputValue, Type factoryType, Type originalType)
        {
            var converterContext = InjectorUtilsInternal.GetConverterContext(inputValue);
            var context = converterContext.BindingContext;
            if (converterContext.IsFixed && !IsCacheContextWithBinding && converterContext.Binding == null)
                return InjectorUtils
                    .ReflectionAccessProvider
                    .ConvertToProxyDelegate(new FactoryResolve(converterContext).Invoke, factoryType);

            var cacheItem = new FactoryDelegateCache(factoryType, context, converterContext.Binding);
            lock (_factoryDelegateCache)
            {
                Delegate dDelegate;
                if (!_factoryDelegateCache.TryGetValue(cacheItem, out dDelegate))
                {
                    context.Service = originalType;
                    dDelegate = InjectorUtils
                        .ReflectionAccessProvider
                        .ConvertToProxyDelegate(new FactoryResolve(converterContext).Invoke, factoryType);
                    _factoryDelegateCache[cacheItem] = dDelegate;
                }
                return dDelegate;
            }
        }

        /// <summary>
        ///     Converts the specified object to lazy-proxy.
        /// </summary>
        /// <param name="inputValue">
        ///     The specified object to convert, by default is always <see cref="IBindingContext" />.
        /// </param>
        /// <param name="lazyType">The specified type of lazy.</param>
        /// <param name="originalType">The specified type of object.</param>
        /// <param name="createLazyProxy">The specified delegate to create lazy-proxy.</param>
        /// <param name="delegateType">The specified delegate type.</param>
        /// <returns>An instance of lazy-proxy.</returns>
        protected virtual object ConvertToLazy(object inputValue, Type lazyType, Type originalType, ActivatorDelegate createLazyProxy,
                                               Type delegateType)
        {
            var converterContext = InjectorUtilsInternal.GetConverterContext(inputValue);
            var context = converterContext.BindingContext;
            if (converterContext.IsFixed && converterContext.Binding == null)
                return createLazyProxy(InjectorUtils
                                           .ReflectionAccessProvider
                                           .ConvertToProxyDelegate(new FactoryResolve(converterContext).Invoke,
                                                                   delegateType));

            var delegateCache = new FactoryDelegateCache(lazyType, context, converterContext.Binding);
            lock (_factoryDelegateCache)
            {
                Delegate dDelegate;
                if (!_factoryDelegateCache.TryGetValue(delegateCache, out dDelegate))
                {
                    context.Service = originalType;
                    dDelegate = InjectorUtils
                        .ReflectionAccessProvider
                        .ConvertToProxyDelegate(new FactoryResolve(converterContext).Invoke, delegateType);
                    _factoryDelegateCache[delegateCache] = dDelegate;
                }
                return createLazyProxy(dDelegate);
            }
        }

        #endregion

        #region Overrides of ConverterBase

        /// <summary>
        ///     Gets the supported types.
        /// </summary>
        public override IEnumerable<Type> SupportedTypes
        {
            get { return InjectorUtilsInternal.Concat(_factoryDelegateTypes, _lazyDelegateTypes); }
        }

        /// <summary>
        ///     Tries to convert instance.
        /// </summary>
        /// <param name="bindingContext">The specified type to convert from.</param>
        /// <param name="originalType">The type to converted to.</param>
        /// <param name="convertAction">An delegate to convert instance.</param>
        /// <returns>
        ///     If <c>true</c> can be converted; otherwise <c>false</c>.
        /// </returns>
        protected override ConverterResult TryConvertInternal(IBindingContext bindingContext, out Type originalType, out Func<object, object> convertAction)
        {
            var service = bindingContext.Service;
            if (IsFactory(service, out originalType))
            {
                Type origType = originalType;
                convertAction = o => ConvertToFunc(o, service, origType);
                return ConverterResult.SupportedWithCache;
            }
            ConstructorInfo constructor;
            if (IsLazyProxy(service, out originalType, out constructor))
            {
                ParameterInfo[] parameters = constructor.GetParameters();
                if (parameters.Length == 1)
                {
                    Type origType = originalType;
                    ActivatorDelegate activatorDelegate =
                        InjectorUtils.ReflectionAccessProvider.GetActivatorDelegate(constructor);
                    convertAction = o => ConvertToLazy(o, service, origType, activatorDelegate, parameters[0].ParameterType);
                    return ConverterResult.SupportedWithCache;
                }
            }

            convertAction = null;
            return ConverterResult.NotSupported;
        }

        /// <summary>
        /// Clears cache, if any.
        /// </summary>
        protected override void ClearCacheInternal()
        {
            lock (_factoryDelegateCache)
                _factoryDelegateCache.Clear();
        }

        #endregion
    }
}